import type { Metadata } from 'next'; import Link from 'next/link'; import { notFound } from 'next/navigation'; import { Suspense } from 'react'; import { PageHeader } from '@/components/ui/page-header'; import { Card, CardHeader, Delta, EmptyState, Skeleton, Stat, Badge, Unavailable } from '@/components/ui/primitives'; import { MarketTable } from '@/components/market/market-table'; import { AssetBrowser } from '@/components/market/asset-browser'; import { Thumb } from '@/components/market/bits'; import type { SP } from '@/lib/search-params'; import { AssetTable } from '@/components/market/asset-list'; import { SalesTable, ListingsTable } from '@/components/market/sales-table'; import { LotsIntelTable } from '@/components/market/lots-table'; import { LineChart } from '@/components/charts/line-chart'; import { BarChart } from '@/components/charts/bar-chart'; import { getBrandsInCategory, getCategoryRow, getCategorySeries, getMarketRows, getPopulationTrend, getSetsInCategory } from '@/lib/queries/markets'; import { rankedAssets, categoryScope } from '@/lib/queries/assets'; import { getRecentSalesInScope, getTopSales, listListings, listLots, listNews, listRadar } from '@/lib/queries/market-lists'; import { getIndex } from '@/lib/queries/indices'; import { fmtMoney, fmtNum, fmtRelative, fmtDate } from '@/lib/format'; import { categoryCrumbs, getCategory, humanize } from '@/lib/taxonomy'; import { CATEGORIES } from '@rareindex/taxonomy'; export const revalidate = 300; export async function generateStaticParams() { return CATEGORIES.filter((c) => c.phase === 1).map((c) => ({ slug: c.slug })); } export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise { const { slug } = await params; const c = getCategory(slug); if (!c) return { title: 'Market not found' }; return { title: `${c.name} market`, description: `${c.name} collectibles market on RareIndex: index, sales, volume, top movers, most valuable assets, listings and news.` }; } export default async function MarketPage({ params, searchParams }: { params: Promise<{ slug: string }>; searchParams: Promise }) { const { slug } = await params; const sp = await searchParams; const row = await getCategoryRow(slug); if (!row) notFound(); const { node, snapshot, counts } = row; const index = node.index ? await getIndex(node.index) : null; const subcategories = CATEGORIES.filter((c) => c.parent === slug); return (
Phase {node.phase} {index ? ( Subindex {index.ticker} ) : null} {node.compliance?.length ? {node.compliance.map(humanize).join(' · ')} : null} Explore assets → } />
{[ ['Tracked assets', fmtNum(counts.assets)], ['With valuation', fmtNum(counts.priced)], ['Sales (all time)', fmtNum(counts.sales)], ['Sales 30D', fmtNum(counts.sales30d)], ['Volume 30D', counts.volume30dUsd != null ? fmtMoney(counts.volume30dUsd, 'USD', { compact: true }) : '—'], ['Active listings', fmtNum(counts.listings)], ['Median RIV', counts.medianRivUsd != null ? fmtMoney(counts.medianRivUsd) : '—'], ['Market cap est.', snapshot?.marketCapEstUsd != null ? fmtMoney(snapshot.marketCapEstUsd, 'USD', { compact: true }) : '—'], ].map(([k, v]) => (
))}
{snapshot?.marketCapEstUsd != null ?

Market cap estimate = estimated population × representative RIV per asset, summed; indicative only (§124).

: null} {counts.assets > 0 && counts.priced === 0 ? (

{fmtNum(counts.assets)} {node.name} assets are catalogued. Valuations publish per asset once enough verified sales exist; guide prices from price-guide sources are shown in the meantime, labelled as such (never presented as market value).

) : null}
}>
{counts.sales > 0 || counts.listings > 0 ? ( <> }> }> }> ) : (

Market analytics publish once verified sales or live listings exist in {node.name}.

Movers, volume, records, value opportunities, auctions and the category index all derive from observed transactions. Until then the catalogue above is complete and guide prices are shown where a price-guide source reports them (labelled, never presented as market value).{' '} How coverage grows →

)} }>
); } async function IndexPanel({ slug }: { slug: string }) { const series = await getCategorySeries(slug, 730); const points = series.filter((p) => p.indexValue != null).map((p) => ({ x: p.date, y: p.indexValue! })); const salesSeries = series.map((p) => ({ x: p.date, y: p.sales })); return (

Category index

Daily category index from valuations of priced constituents; publishes once enough assets are priced.

Daily sales

Verified transactions per day (count)

); } async function Movers({ slug }: { slug: string }) { const scope = categoryScope(slug); const [gainers, losers, volume, expensive, trending] = await Promise.all([ rankedAssets('gainers', { scope, limit: 8, window: '7d' }), rankedAssets('losers', { scope, limit: 8, window: '7d' }), rankedAssets('volume', { scope, limit: 8 }), rankedAssets('expensive', { scope, limit: 8 }), rankedAssets('trending', { scope, limit: 8 }), ]); const block = (title: string, sub: string, items: typeof gainers, columns: Parameters[0]['columns']) => ( ); return (
{block('Top gainers · 7D', 'Assets with ≥3 sales used in valuation', gainers, ['riv', 'change7d', 'change30d', 'sales'])} {block('Top losers · 7D', 'Assets with ≥3 sales used in valuation', losers, ['riv', 'change7d', 'change30d', 'sales'])} {block('Highest volume · 30D', 'Sum of verified sale prices, USD', volume, ['riv', 'sales30d', 'change30d', 'liquidity'])} {block('Most valuable', 'Ranked by RareIndex Valuation', expensive, ['riv', 'latestSale', 'change1y', 'rarity'])} {block('Trending', 'Price × volume × listing momentum', trending, ['riv', 'trending', 'change7d', 'sales30d'])}
); } async function Activity({ slug }: { slug: string }) { const scope = categoryScope(slug); const [records, recent, listings, lots, news, radar, pop] = await Promise.all([ getTopSales(6, scope), getRecentSalesInScope(scope, 10), listListings({ category: slug, pageSize: 10, sort: 'newest' }), listLots({ category: slug, limit: 6, sort: 'value' }), listNews({ category: slug, limit: 6 }), listRadar({ scope, limit: 6 }), getPopulationTrend(slug), ]); const popByGrader = new Map>(); for (const p of pop) (popByGrader.get(p.grader) ?? popByGrader.set(p.grader, []).get(p.grader)!).push({ x: p.reportDate, y: p.total }); return (
All records →} /> All sales →} /> All listings →} /> Auctions →} /> {popByGrader.size ? (
({ id: g, label: g.toUpperCase(), points: pts }))} />
) : ( )}
Open radar →} /> {radar.length ? (
    {radar.map((r) => (
  • {r.assetTitle} {humanize(r.kind)} · {fmtRelative(r.detectedAt)}
  • ))}
) : ( )}
All news →} /> {news.items.length ? (
    {news.items.map((n) => (
  • {n.title} {n.sourceName} · {n.publishedAt ? fmtDate(n.publishedAt) : 'undated'}
  • ))}
) : ( )}
); } async function Structure({ slug }: { slug: string }) { const [children, sets, brands] = await Promise.all([getMarketRows(slug), getSetsInCategory(slug, 40), getBrandsInCategory(slug, 30)]); return (
{children.length ? ( ) : null}
= 40 ? '+' : ''} releases catalogued · ranked by sales, then size`} action={Browse by set →} /> {sets.length ? (
    {sets.map((s) => (
  • {s.name} {s.code ? {s.code} : null} {s.releaseYear ? {s.releaseYear} : null} {fmtNum(s.assets)}{s.sales ? ` · ${fmtNum(s.sales)} sales` : ''}{s.priced ? ` · ${fmtNum(s.priced)} priced` : ''}
  • ))}
) : ( )}
{brands.length ? (
({ label: b.brand, value: b.assets, sublabel: `${b.priced} priced · median RIV ${b.medianRivUsd != null ? fmtMoney(b.medianRivUsd) : '—'}`, href: `/brand/${encodeURIComponent(b.brand)}` }))} />
) : ( )}

Unavailable metrics render as rather than estimates (§192).

); }